#include <map>
#include <SFML/System/Vector2.hpp>
#include <SFML/Graphics/Text.hpp>
#include <nice2d/bitmask.h>
#include <nice2d/tilemap.h>
#include <nice2d/sprite.h>
#include <nice2d/tilemappriv.h>
#include <nice2d/application.h>
#include <nice2d/tostring.h>
#include <nice2d/components/imagemanager.h>

class TileMapMessageListener : public MessageListener
{
public:
    TileMapMessageListener(TileMap &map):
        tileMap(map)
    {
    }
public:
    bool onListener(std::shared_ptr<Message> message)
    {
        if (message->getMessageType() == MessageType_SFML) {
            auto window = Application::getInstance()->getWindow();
            auto mousePosition = sf::Mouse::getPosition(*window);
            auto position = tileMap.getTileIndexByWorldPosition(mousePosition.x, mousePosition.y);

            if (sf::Mouse::isButtonPressed(sf::Mouse::Left) ||
                    sf::Mouse::isButtonPressed(sf::Mouse::Middle) ||
                    sf::Mouse::isButtonPressed(sf::Mouse::Right))
                tileMap.tileClicked(position.x, position.y);
        }
        return false;
    }
private:
    TileMap &tileMap;
};

class TileMapData
{
public:
    float tileSize = 32.0f;
    sf::Vector2i mapSize = sf::Vector2i(64, 64);
    std::string tileImageDescriptor;
    std::map<uint32_t, std::shared_ptr<Tile>> tiles;
    bool textVisible = false;
    std::map<uint32_t, std::shared_ptr<sf::Text>> texts;
    std::shared_ptr<sf::Font> font;
};

IMPLEMENT_OBJECT(TileMap, Node)

std::shared_ptr<TileMap> TileMap::create(TileMapType type)
{
    if (type == TileMapType_Hex)
        return std::make_shared<HexTileMap>();
    else if (type == TileMapType_Grid)
        return std::make_shared<Rect4TileMap>();
    else if (type == TileMapType_MalGrid)
        return std::make_shared<MalRect4TileMap>();
    return nullptr;
}

TileMap::TileMap():
    data(new TileMapData)
{
    auto listener = std::make_shared<TileMapMessageListener>(*this);
    registerMessageListener(listener);
}

TileMap::~TileMap()
{
}

void TileMap::accept(std::function<void (int32_t, int32_t, std::shared_ptr<Tile>)> visitor)
{
    if (!visitor)
        return;

    auto itr = data->tiles.begin();
    while (itr != data->tiles.end()) {
        int x = GET_TILE_X(itr->first);
        int y = GET_TILE_Y(itr->first);
        visitor(x, y, itr->second);
        itr ++;
    }
}

void TileMap::accept(TileVisitor *visitor)
{
    if (!visitor)
        return;

    auto itr = data->tiles.begin();
    while (itr != data->tiles.end()) {
        int x = GET_TILE_X(itr->first);
        int y = GET_TILE_Y(itr->first);
        visitor->visit(x, y, itr->second);
        itr ++;
    }
}

void TileMap::accpetEdge(std::function<void (const sf::Vector2i &, const sf::Vector2i &)> visitor)
{
    if (!visitor)
        return;

    std::set<uint64_t> flags;

    auto itr = data->tiles.begin();
    while (itr != data->tiles.end()) {
        int x = GET_TILE_X(itr->first);
        int y = GET_TILE_Y(itr->first);

        auto list = getAdjacentTileByTileIndex(x, y);
        for (auto item : list) {
            auto current = getTileByIndex(item);
            if (current) {
                uint64_t id = GET_TILE_POSITION(x, y) | (uint64_t)item.x << 32 | (uint64_t)item.y << 48;
                if (flags.find(id) == flags.end()) {
                    flags.insert(id);
                    visitor({x, y}, {item});
                }
            }
        }
        itr ++;
    }
}

bool TileMap::init(int width, int height, float tilesize)
{
    data->font = std::make_shared<sf::Font>();
    data->font->loadFromFile("./../data/font/arial.ttf");

    data->tileSize = tilesize * 0.5f;
    data->mapSize.x = width;
    data->mapSize.y = height;

    for (int j = 0; j < height; j++) {
        for (int i = 0; i < width; i++) {
            auto tile = createTile(i, j, data->tileSize);
            tile->setOutlineThickness(2.0);
            tile->setFillColor(sf::Color::Black);
            tile->setOutlineColor(sf::Color::Green);
            tile->setOutlineThickness(1.0);
            size_t id = GET_TILE_POSITION(i, j);
            data->tiles.insert(std::make_pair(id, tile));

            auto text = std::make_shared<sf::Text>();
            text->setFont(*data->font);
            text->setCharacterSize(12);
            text->setFillColor(sf::Color::White);

            std::wstring ss;
            ss = std::to_wstring(i);
            ss += L",";
            ss += std::to_wstring(j);
            text->setString(ss);

            auto box = text->getLocalBounds();
            auto center = tile->getPosition();
            text->setPosition(center.x - box.width * 0.5f, center.y - box.height * 0.5f);
            data->texts.insert(std::make_pair(id, text));
        }
    }

    return true;
}

void TileMap::setImageDescriptor(const std::string &file)
{
    data->tileImageDescriptor = file;
}

std::string TileMap::getImageDescriptor() const
{
    return data->tileImageDescriptor;
}

TileMapType TileMap::getTileMapType() const
{
    return TileMapType_Grid;
}

float TileMap::getTileSize()const
{
    return data->tileSize;
}

sf::Vector2i TileMap::getTileMapSize()const
{
    return data->mapSize;
}

std::shared_ptr<Tile> TileMap::getTileByIndex(const sf::Vector2i &index)
{
    return getTileByIndex(index.x, index.y);
}

std::shared_ptr<Tile> TileMap::getTileByIndex(int x, int y)
{
    auto id = GET_TILE_POSITION(x, y);
    auto find = data->tiles.find(id);
    return find == data->tiles.end() ? nullptr : find->second;
}

void TileMap::setTextVisible(bool visible)
{
    data->textVisible = visible;
}

bool TileMap::isTextVisible()const
{
    return data->textVisible;
}

PTree TileMap::serialize()
{
    PTree node;
    SERIALIZE_BASE_OBJECT(node, Node)

    node.add("type", toString(getTileMapType()));
    node.add("imageDescriptor", getImageDescriptor());

    PTree size;
    size.add("tileSize", data->tileSize * 2.0f);
    size.add("width", data->mapSize.x);
    size.add("height", data->mapSize.y);
    node.add_child("size", size);

    node.add("textVisible", isTextVisible());

    class MyTileVisitor : public TileVisitor
    {
    public:
        MyTileVisitor() = delete;
        MyTileVisitor(PTree &inputNode):
            node(inputNode)
        {
        }
    public:
        void visit(int32_t x, int32_t y, std::shared_ptr<Tile> tile) override
        {
            PTree current;
            current.add("x", x);
            current.add("y", y);
            current.add_child("item", tile->serialize());
            node.add_child("tile", current);
        }
    private:
        PTree &node;
    };

    PTree tiles;
    MyTileVisitor visitor(tiles);
    accept(&visitor);
    node.add_child("tiles", tiles);

    return node;
}

void TileMap::deserialize(const PTree &node)
{
    DESERIALIZE_BASE_OBJECT(node, Node);

    auto sizeNode = node.get_child("size");
    float tileSize = sizeNode.get<float>("tileSize");
    int32_t width = sizeNode.get<int32_t>("width");
    int32_t height = sizeNode.get<int32_t>("height");

    init(width, height, tileSize);

    setTextVisible(node.get<bool>("textVisible"));
    setImageDescriptor(node.get<std::string>("imageDescriptor"));

    //auto imageDescriptor = DeserializerFactory::deserialize<ImageDescriptor>("ImageDescriptor",
    //                                                                         getImageDescriptor());
    auto textureManager = GET_COMPONENT(Application::getInstance(), ImageManager);


    auto tiles = node.get_child("tiles");
    auto itr = tiles.begin();
    while (itr != tiles.end()) {
        auto item = itr->second;
        int32_t x = item.get<int32_t>("x");
        int32_t y = item.get<int32_t>("y");
        auto tile = getTileByIndex(x, y);
        tile->deserialize(item.get_child("item"));

        auto resourceId = tile->getTileResourceId();
        /*if (imageDescriptor) {
            auto area = imageDescriptor->areas[resourceId];
            auto texture = textureManager->loadTextureFromFile(imageDescriptor->file, area);
            tile->setTexture(texture.get());
        }*/
        itr ++;
    }
}

void TileMap::onDrawNode(sf::RenderTarget &target, sf::RenderStates states) const
{
    states.transform *= getTransform();
    auto itr = data->tiles.begin();
    while (itr != data->tiles.end()) {
        auto tile = itr->second;
        if (tile && tile->isVisible())
            target.draw(*tile, states);
        itr ++;
    }

    if (data->textVisible) {
        for (const auto &text : data->texts) {
            target.draw(*text.second, states);
        }
    }
}
